Análisis de Indicadores de Rendimiento¶

Objetivo: Evaluar el rendimiento de la aplicación mediante el análisis de métricas clave extraídas de la base de datos.

Este reporte transforma los datos crudos en información accionable para identificar:

  1. Tendencias de uso.
  2. Estructura del contenido educativo.
  3. Niveles de engagement de los usuarios.
In [3]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns # Opcional: Para gráficos más estéticos

# Configuración de estilo
plt.style.use('ggplot') 
plt.rcParams['figure.figsize'] = (10, 6)

# Carga de datos
try:
    droped_df = pd.read_csv("P20242_Inst_1_clean.csv")
    print("Datos cargados correctamente.")
    print(f"Registros totales: {droped_df.shape[0]}")
except FileNotFoundError:
    print("Error: El archivo 'cleaned_data.csv' no se encuentra.")
Datos cargados correctamente.
Registros totales: 1162146

1. Resumen de Métricas Globales¶

Vista general del volumen de usuarios y contenido disponible en la plataforma.

In [4]:
# Cálculos básicos
unique = droped_df[['page', 'artifact']].drop_duplicates().reset_index(drop=True)
total_artifacts = unique.groupby('page').size().reset_index(name='artifact_count')
unique_pages = droped_df[['chapter', 'page']].drop_duplicates().reset_index(drop=True)
total_pages_per_chapter = unique_pages.groupby('chapter').size().reset_index(name='pages')
pages_to_chapter = droped_df[['chapter', 'page']].drop_duplicates().reset_index(drop=True)



usuarios_unicos = droped_df['_idUser'].nunique()
total_capitulos = droped_df['chapter'].nunique()
paginas_unicas = total_pages_per_chapter['pages'].sum()
artefactos_unicos = total_artifacts.merge(pages_to_chapter, on='page', how='left').groupby('chapter')['artifact_count'].sum().reset_index(name='artifacts')['artifacts'].sum()

# Mostrar KPIs
print(f"--- KPIs Principales ---")
print(f"Usuarios Únicos: {usuarios_unicos}")
print(f"Capítulos Totales: {total_capitulos}")
print(f"Páginas Totales: {paginas_unicas}")
print(f"Artefactos Únicos: {artefactos_unicos}")
--- KPIs Principales ---
Usuarios Únicos: 1079
Capítulos Totales: 2
Páginas Totales: 48
Artefactos Únicos: 399

2. Estructura del Contenido¶

Analizamos cómo se distribuye el contenido educativo a través de los capítulos. Esto nos ayuda a entender la carga de trabajo teórica para el estudiante.

In [5]:
# Cálculos de páginas y artefactos por capítulo

unique_artifacts = droped_df[['page', 'artifact']].drop_duplicates().reset_index(drop=True)
total_artifacts = unique_artifacts.groupby('page').size().reset_index(name='artifact_count')

# Unir para obtener artefactos por capítulo
pages_to_chapter = droped_df[['chapter', 'page']].drop_duplicates().reset_index(drop=True)
total_artifacts_per_chapter = total_artifacts.merge(pages_to_chapter, on='page', how='left')\
                                             .groupby('chapter')['artifact_count'].sum()\
                                             .reset_index(name='artifacts')

print("Distribución de contenido por capítulo:")
display(total_pages_per_chapter.head()) # Use display() si está en Jupyter
Distribución de contenido por capítulo:
chapter pages
0 0 7
1 1 41
In [6]:
# Gráfico: Páginas por Capítulo
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.bar(total_pages_per_chapter['chapter'].astype(str), total_pages_per_chapter['pages'], color='#3498db')
plt.title('Páginas por Capítulo')
plt.xlabel('Capítulo')
plt.ylabel('Cantidad de Páginas')

# Gráfico: Artefactos por Capítulo
plt.subplot(1, 2, 2)
plt.bar(total_artifacts_per_chapter['chapter'].astype(str), total_artifacts_per_chapter['artifacts'], color='#e74c3c')
plt.title('Artefactos por Capítulo')
plt.xlabel('Capítulo')
plt.ylabel('Cantidad de Artefactos')

plt.tight_layout()
plt.show()
No description has been provided for this image

3. Comportamiento y Progreso del Usuario¶

Evaluamos qué tan lejos llegan los usuarios. ¿Completan todos los capítulos o abandonan al principio?

In [7]:
# Capítulos y Páginas por Usuario
chapters_per_user = droped_df.groupby('_idUser')['chapter'].nunique().reset_index(name='chapters')
pages_per_user = droped_df.groupby('_idUser')['page'].nunique().reset_index(name='pages')

# Promedios
avg_chapters = chapters_per_user['chapters'].mean()
avg_pages = pages_per_user['pages'].mean()

print(f"Promedio de capítulos visitados por usuario: {avg_chapters:.2f}")
print(f"Promedio de páginas visitadas por usuario: {avg_pages:.2f}")
Promedio de capítulos visitados por usuario: 1.80
Promedio de páginas visitadas por usuario: 32.66
In [8]:
# Histogramas de Progreso
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.hist(chapters_per_user['chapters'], bins=range(1, int(chapters_per_user['chapters'].max()) + 2), 
         edgecolor='white', color='#9b59b6')
plt.title('Distribución: Capítulos por Usuario')
plt.xlabel('Número de Capítulos')
plt.ylabel('Frecuencia (Usuarios)')

plt.subplot(1, 2, 2)
plt.hist(pages_per_user['pages'], bins=20, edgecolor='white', color='#8e44ad')
plt.title('Distribución: Páginas por Usuario')
plt.xlabel('Número de Páginas')
plt.ylabel('Frecuencia')

plt.tight_layout()
plt.show()
No description has been provided for this image

4. Interacción y Reintentos¶

Analizamos la cantidad de ejercicios completados y los reintentos (artefactos repetidos), lo cual puede ser un indicador de la dificultad del contenido o del compromiso del usuario.

In [9]:
# Ejercicios completos
ejercicios_completos = droped_df[droped_df['validationArtifact'] == True].groupby('_idUser').size().reset_index(name='completed')

# Reintentos (Artefactos repetidos)
artifact_counts = droped_df.groupby(['_idUser', 'artifact']).size().reset_index(name='count')
reintentos = artifact_counts[artifact_counts['count'] > 1].copy()
reintentos['reintentos_count'] = reintentos['count'] - 1
total_reintentos = reintentos.groupby('_idUser')['reintentos_count'].sum().reset_index(name='total_reintentos')

print(f"Promedio de ejercicios completados: {ejercicios_completos['completed'].mean():.2f}")
print(f"Promedio de reintentos por usuario: {total_reintentos['total_reintentos'].mean():.2f}")
Promedio de ejercicios completados: 376.02
Promedio de reintentos por usuario: 1078.07
In [10]:
plt.figure(figsize=(12, 5))

# Ejercicios Completos
plt.subplot(1, 2, 1)
plt.hist(ejercicios_completos['completed'], bins=20, edgecolor='white', color='#2ecc71')
plt.title('Ejercicios Completos por Usuario')
plt.xlabel('Cant. Ejercicios')
plt.ylabel('Usuarios')

# Reintentos
plt.subplot(1, 2, 2)
plt.hist(total_reintentos['total_reintentos'], bins=20, edgecolor='white', color='#f1c40f')
plt.title('Distribución de Reintentos')
plt.xlabel('Cant. Reintentos')
plt.ylabel('Usuarios')

plt.tight_layout()
plt.show()
No description has been provided for this image

5. Tiempo por unidad¶

Al analizar las horas totales consumidas por unidad, se puede determinar el "peso" real de cada sección dentro del curso. Este indicador es vital para detectar cuellos de botella: las páginas o capítulos con picos de horas consumidas sugieren puntos donde los estudiantes se detienen más, ya sea por complejidad pedagógica o porque el contenido es altamente relevante para ellos.

In [11]:
# Cálculos de segundos
total_seconds_per_chapter = droped_df.groupby('chapter')['seconds'].sum().reset_index(name='total_seconds')
total_seconds_per_page = droped_df.groupby('page')['seconds'].sum().reset_index(name='total_seconds')

print("Horas consumidas por capítulo:\n", total_seconds_per_chapter.assign(total_hours=total_seconds_per_chapter['total_seconds'] / 3600))
print("Horas consumidas por página:\n", total_seconds_per_page.assign(total_hours=total_seconds_per_page['total_seconds'] / 3600))
Horas consumidas por capítulo:
    chapter  total_seconds   total_hours
0        0       19280073   5355.575833
1        1       37909541  10530.428056
Horas consumidas por página:
     page  total_seconds  total_hours
0    1.0        2210761   614.100278
1    1.1        3298853   916.348056
2    2.0        4260252  1183.403333
3    2.1        3050365   847.323611
4    3.0        3177939   882.760833
5    3.1          77342    21.483889
6    4.0        1611702   447.695000
7    5.0        3709239  1030.344167
8    6.0        5168030  1435.563889
9    7.0         261738    72.705000
10   8.0        1311647   364.346389
11   9.0         394885   109.690278
12  10.0        1145487   318.190833
13  10.1         638670   177.408333
14  11.0         866275   240.631944
15  12.0        1176706   326.862778
16  13.0        1423052   395.292222
17  14.0         824690   229.080556
18  14.1         617263   171.461944
19  15.0         632389   175.663611
20  16.0         578638   160.732778
21  16.1         578566   160.712778
22  17.0        1642929   456.369167
23  18.0        1521419   422.616389
24  18.1         729955   202.765278
25  19.0         430736   119.648889
26  19.1         120592    33.497778
27  19.2         155517    43.199167
28  20.0         166585    46.273611
29  20.1         369155   102.543056
30  21.0         667583   185.439722
31  22.0        1451069   403.074722
32  23.0         939293   260.914722
33  23.1        1078255   299.515278
34  24.0         756699   210.194167
35  24.1         889271   247.019722
36  25.0        1108353   307.875833
37  26.0        2320598   644.610556
38  27.0         879958   244.432778
39  28.0        1077244   299.234444
40  29.0        1259062   349.739444
41  30.0         854907   237.474167
42  30.1        1755945   487.762500
In [12]:
# Gráfico de horas consumidas por capítulo
plt.figure(figsize=(10, 5))
plt.bar(total_seconds_per_chapter['chapter'].astype(str), total_seconds_per_chapter['total_seconds'] / 3600)
plt.title('Horas Consumidas por Capítulo')
plt.xlabel('Capítulo')
plt.ylabel('Horas')
plt.xticks(rotation=45)
plt.show()

# Gráfico de horas consumidas por página
plt.figure(figsize=(10, 5))
plt.bar(total_seconds_per_page['page'].astype(str), total_seconds_per_page['total_seconds'] / 3600)
plt.title('Horas Consumidas por Página')
plt.xlabel('Página')
plt.ylabel('Horas')
plt.xticks(rotation=45)
plt.show()
No description has been provided for this image
No description has been provided for this image

Comportamiento del tiempo¶

In [13]:
media = droped_df["seconds"].mean()
variacion = droped_df["seconds"].std()


limite = media +  2 * variacion 
print("La media es de: ", media)
print("La variacion estandar: ", variacion)

droped_df = droped_df[droped_df['seconds'] <= limite].copy()
# df_filtered.to_csv('output.csv', index=False)
La media es de:  49.21035222768912
La variacion estandar:  193.16735802815361

El uso de la media (μ) y la desviación estándar (σ) es un método estadístico fundamental y ampliamente adoptado para la detección y gestión de valores atípicos (outliers) en un conjunto de datos. Al filtrar las entradas que se pasen de dos veces la variacion estandar más la media podemos descartar las anomalias que se encuentran en el 5% de la distribucion.

Comportamiento por artefacto¶

In [14]:
# 1. Preparación de los datos
# Agrupamos por capítulo, página y artefacto, contando usuarios únicos
interaction_counts = droped_df.groupby(['chapter', 'page', 'artifact'])['_idUser'].nunique().reset_index()
interaction_counts.rename(columns={'_idUser': 'unique_users'}, inplace=True)

# Creamos una etiqueta compuesta para el eje X (Ej: C1-P1.1-ArtA)
# Esto ayuda a que la gráfica sea legible si hay muchos artefactos
interaction_counts['label'] = (
    "C" + interaction_counts['chapter'].astype(str) + 
    "-P" + interaction_counts['page'].astype(str) + 
    "-" + interaction_counts['artifact'].astype(str)
)

# Ordenamos para mantener la secuencia lógica del curso
interaction_counts.sort_values(by=['chapter', 'page', 'artifact'], inplace=True)

# 2. Configuración de la Gráfica
plt.figure(figsize=(14, 8))
sns.set_style("whitegrid")

# Crear el gráfico de barras
barplot = sns.barplot(
    data=interaction_counts,
    x='label',
    y='unique_users',
    hue='chapter',  # Colorear por capítulo para distinguir secciones visualmente
    dodge=False,    # Las barras se mantienen alineadas
    palette='viridis'
)

# 3. Personalización
plt.title('Cantidad de Usuarios Únicos por Artefacto (Secuencial)', fontsize=16)
plt.xlabel('Artefacto (Capítulo - Página - ID)', fontsize=12)
plt.ylabel('Número de Usuarios Únicos', fontsize=12)

# Rotar las etiquetas del eje X para que no se superpongan
plt.xticks(rotation=90, fontsize=8)

# Añadir una línea de promedio para referencia
avg_users = interaction_counts['unique_users'].mean()
plt.axhline(avg_users, color='red', linestyle='--', label=f'Promedio ({int(avg_users)} usuarios)')
plt.legend()

plt.tight_layout()
plt.show()
No description has been provided for this image
In [15]:
# 1. Filtrado y Preparación
# Filtramos solo donde validationArtifact es True (el usuario resolvió el ejercicio)
completed_data = droped_df[droped_df['validationArtifact'] == True]

# Agrupamos por jerarquía
success_counts = completed_data.groupby(['chapter', 'page', 'artifact'])['_idUser'].nunique().reset_index()
success_counts.rename(columns={'_idUser': 'successful_users'}, inplace=True)

# Crear etiqueta legible (C#-P#-Art#)
success_counts['label'] = (
    "C" + success_counts['chapter'].astype(str) + 
    "-P" + success_counts['page'].astype(str) + 
    "-" + success_counts['artifact'].astype(str)
)

# Ordenar secuencialmente
success_counts.sort_values(by=['chapter', 'page', 'artifact'], inplace=True)

# 2. Configuración de la Gráfica
plt.figure(figsize=(14, 8))
sns.set_style("whitegrid")

# Usamos una paleta de verdes para denotar "éxito"
success_plot = sns.barplot(
    data=success_counts,
    x='label',
    y='successful_users',
    hue='chapter',
    dodge=False,
    palette='Greens_d' # Paleta verde oscura
)

# 3. Personalización
plt.title('Usuarios que COMPLETARON exitosamente cada Artefacto', fontsize=16)
plt.xlabel('Artefacto (Capítulo - Página - ID)', fontsize=12)
plt.ylabel('Cantidad de Usuarios Aprobados', fontsize=12)
plt.xticks(rotation=90, fontsize=8)

# Línea de promedio de aprobados
avg_success = success_counts['successful_users'].mean()
plt.axhline(avg_success, color='orange', linestyle='--', label=f'Promedio de Éxito ({int(avg_success)} usuarios)')
plt.legend()

plt.tight_layout()
plt.show()
No description has been provided for this image
In [16]:
# 1. Filtrado y Preparación
# Filtramos donde validationArtifact es False (error o incompleto)
failed_data = droped_df[droped_df['validationArtifact'] == False]

# Agrupamos por jerarquía
fail_counts = failed_data.groupby(['chapter', 'page', 'artifact'])['_idUser'].nunique().reset_index()
fail_counts.rename(columns={'_idUser': 'failed_users'}, inplace=True)

# Crear etiqueta legible
fail_counts['label'] = (
    "C" + fail_counts['chapter'].astype(str) + 
    "-P" + fail_counts['page'].astype(str) + 
    "-" + fail_counts['artifact'].astype(str)
)

# Ordenar secuencialmente
fail_counts.sort_values(by=['chapter', 'page', 'artifact'], inplace=True)

# 2. Configuración de la Gráfica
plt.figure(figsize=(14, 8))
sns.set_style("whitegrid")

# Usamos una paleta de Rojos para denotar "error/alerta"
fail_plot = sns.barplot(
    data=fail_counts,
    x='label',
    y='failed_users',
    hue='chapter',
    dodge=False,
    palette='Reds_d' # Paleta de rojos oscuros
)

# 3. Personalización
plt.title('Usuarios con Intentos FALLIDOS en cada Artefacto', fontsize=16)
plt.xlabel('Artefacto (Capítulo - Página - ID)', fontsize=12)
plt.ylabel('Cantidad de Usuarios con Errores', fontsize=12)
plt.xticks(rotation=90, fontsize=8)

# Línea de promedio de fallos (para detectar los casos críticos)
avg_fail = fail_counts['failed_users'].mean()
plt.axhline(avg_fail, color='black', linestyle='--', label=f'Promedio de Fallos ({int(avg_fail)} usuarios)')
plt.legend()

plt.tight_layout()
plt.show()
No description has been provided for this image
In [17]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# Suponiendo que el DataFrame se llama 'df' y tiene las columnas 'date' y 'artifact'
# --- PASO 1: Asegurarse de que la columna 'date' sea datetime y extraer la fecha
droped_df['date'] = pd.to_datetime(droped_df['date'])
droped_df['day'] = droped_df['date'].dt.date

# --- PASO 2: Contar los artefactos por día
artifacts_per_day = droped_df.groupby('day')['artifact'].count().reset_index()
artifacts_per_day.rename(columns={'artifact': 'num_artifacts'}, inplace=True)

# --- PASO 3: Mostrar el resultado
print(artifacts_per_day)

# Para un gráfico de barras simple (que es más fácil que un "calendar plot" sin bibliotecas especializadas)
import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))
plt.bar(artifacts_per_day['day'].astype(str), artifacts_per_day['num_artifacts'], color='skyblue')
plt.title('Cantidad de Artefactos Registrados por Día')
plt.xlabel('Día')
plt.ylabel('Cantidad de Artefactos')
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()


    # 1. Conversión de la columna 'date' a formato datetime
droped_df['date'] = pd.to_datetime(droped_df['date'])

    # 2. Agregación: Contar artefactos por día
artifacts_per_day = droped_df.groupby(droped_df['date'].dt.date)['artifact'].count()
artifacts_per_day.index = pd.to_datetime(artifacts_per_day.index)
    
    # 3. Preparación para el Heatmap Calendar
calendar_df = pd.DataFrame(artifacts_per_day)
calendar_df.columns = ['Count']
    
    # Extraer características temporales
calendar_df['DayOfWeek'] = calendar_df.index.dayofweek # Lunes=0, Domingo=6
calendar_df['WeekOfYear'] = calendar_df.index.isocalendar().week.astype(int)
calendar_df['Year'] = calendar_df.index.year
    
    # Seleccionar el año (si hay varios)
year_to_plot = calendar_df['Year'].min()
df_year = calendar_df[calendar_df['Year'] == year_to_plot]

    # Pivotear la tabla: Filas = Día de la Semana, Columnas = Semana del Año
pivot_table = df_year.pivot_table(
    values='Count', 
    index='DayOfWeek', 
    columns='WeekOfYear', 
    fill_value=0
)
    
    # 4. Generación del Heatmap
day_labels = ['Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb', 'Dom']
pivot_table.index = day_labels

plt.figure(figsize=(18, 5))
    
sns.heatmap(
    pivot_table,
    cmap="Greens", # Color similar a GitHub
    linewidths=0.5,
    linecolor='white',
    square=True,
    cbar_kws={'label': 'Cantidad de Artefactos'},
    yticklabels=day_labels,
    vmax=pivot_table.values.max() 
)
    
plt.title(f'Frecuencia de Artefactos por Día ({year_to_plot})', fontsize=16)
plt.xlabel('Semana del Año')
plt.ylabel('')
plt.yticks(rotation=0)
    
plt.savefig('calendar_heatmap.png')
plt.show()
C:\Users\crist\AppData\Local\Temp\ipykernel_22292\2990445065.py:8: UserWarning: Parsing dates in %d-%m-%Y format when dayfirst=False (the default) was specified. Pass `dayfirst=True` or specify a format to silence this warning.
  droped_df['date'] = pd.to_datetime(droped_df['date'])
            day  num_artifacts
0    2024-10-25             84
1    2024-10-26            116
2    2024-10-27           1393
3    2024-10-28          16380
4    2024-10-29          41244
..          ...            ...
111  2025-03-14             61
112  2025-03-15              1
113  2025-03-16            260
114  2025-03-17             43
115  2025-03-18             26

[116 rows x 2 columns]
No description has been provided for this image
No description has been provided for this image
In [21]:
import pandas as pd

# 1. Filtrar solo los intentos fallidos
only_failures = droped_df[droped_df['validationArtifact'] == False]

# 2. Contar cuántas veces falló cada usuario en cada artefacto específico
user_fail_counts = only_failures.groupby(['chapter', 'page', 'artifact', '_idUser']).size().reset_index(name='fail_count')

# 3. Calcular "Reintentos Fallidos" (Cualquier fallo después del primero)
# Si falló 3 veces: 1er intento (error) + 2 reintentos (errores) -> Valor = 2
user_fail_counts['failed_retries'] = user_fail_counts['fail_count'] - 1

# Filtrar para dejar solo aquellos donde hubo persistencia en el error
blocking_events = user_fail_counts[user_fail_counts['failed_retries'] > 0]

# 4. Agrupar por artefacto para obtener el puntaje total de bloqueo
top_blocking = blocking_events.groupby(['chapter', 'page', 'artifact'])['failed_retries'].sum().reset_index()

# Crear etiqueta legible
top_blocking['Artefacto_ID'] = (
    "C" + top_blocking['chapter'].astype(str) + 
    "-P" + top_blocking['page'].astype(str) + 
    "-" + top_blocking['artifact'].astype(str)
)

# 5. Ordenar de mayor a menor bloqueo y tomar los top 25
top_25_blocking = top_blocking.sort_values(by='failed_retries', ascending=False).head(25)

# Limpiar la tabla para mostrar
final_list = top_25_blocking[['Artefacto_ID', 'failed_retries']].reset_index(drop=True)
final_list.columns = ['Identificador (C-P-A)', 'Total Reintentos Fallidos']

# Mostrar la lista
print("=== TOP 25 ARTEFACTOS BLOQUEANTES ===")
print(final_list)

# Opcional: Si quieres exportarlo a CSV para revisarlo luego
# final_list.to_csv('top_25_bloqueantes.csv', index=False)
=== TOP 25 ARTEFACTOS BLOQUEANTES ===
   Identificador (C-P-A)  Total Reintentos Fallidos
0             C1-P13.0-2                      10587
1              C0-P6.0-4                       8995
2             C1-P18.0-7                       8135
3              C1-P8.0-6                       7691
4              C0-P6.0-2                       7558
5             C1-P17.0-6                       7158
6             C1-P30.1-3                       6875
7             C1-P12.0-4                       6713
8             C0-P2.1-10                       6701
9              C0-P1.1-1                       6165
10            C1-P25.0-8                       6097
11             C0-P6.0-3                       5972
12             C1-P2.0-2                       5926
13             C0-P6.0-6                       5884
14            C1-P18.0-8                       5601
15            C1-P17.0-1                       5535
16            C1-P26.0-1                       5433
17            C1-P29.0-8                       5378
18             C0-P5.0-5                       5334
19            C1-P22.0-3                       5308
20             C0-P2.1-6                       5219
21            C1-P11.0-3                       5071
22             C0-P6.0-5                       4936
23             C0-P2.0-4                       4875
24            C1-P29.0-9                       4769
In [22]:
import pandas as pd
import matplotlib.pyplot as plt

# Conteo de registros para cada valor de la columna 'originMovil'
conteo_origen = droped_df['originMovil'].value_counts()

# Número total de intentos 
total_intentos = droped_df.shape[0]

# Etiquetas y colores
labels_raw = conteo_origen.index.tolist()
labels_map = {True: 'Origen Móvil', False: 'Otro Origen'}
labels = [labels_map[val] for val in labels_raw]

colors = ['#4CAF50', '#FF9800'] 

# 3. VISUALIZACIÓN
plt.figure(figsize=(6, 6))

plt.pie(
    conteo_origen.values,
    labels=labels,
    colors=colors[:len(labels)],
    # Muestra el porcentaje en cada porción con 1 decimal
    autopct='%1.1f%%', 
    wedgeprops={'edgecolor': 'black', 'linewidth': 1, 'antialiased': True},
)

# Número total de intentos
plt.title(f'Distribución de Intentos por Origen (Móvil vs. Otro)\nTotal de Intentos: {total_intentos}', fontsize=14)
plt.axis('equal') 

plt.tight_layout()
plt.show()

# Resultados
print("-" * 40)
print(f"Número Total de Intentos (Registros): {total_intentos}")
print(f"Distribución Detallada:")
for i, count in enumerate(conteo_origen):
    print(f"- {labels[i]}: {count} ({conteo_origen.iloc[i] / total_intentos:.1%})")
print("-" * 40)
No description has been provided for this image
----------------------------------------
Número Total de Intentos (Registros): 1151002
Distribución Detallada:
- Origen Móvil: 813640 (70.7%)
- Otro Origen: 337362 (29.3%)
----------------------------------------
In [23]:
import pandas as pd
import numpy as np 

droped_df['minutes'] = droped_df['seconds'] / 60

droped_df['label'] = (
        "C" + droped_df['chapter'].astype(str) +
        "-P" + droped_df['page'].astype(str) +
        "-" + droped_df['artifact'].astype(str)
)

# 1. PREPARACIÓN NIVEL USUARIO
user_interactions = droped_df.groupby(['_idUser', 'label', 'chapter']).agg(
    visits=('label', 'count'),          # Cuántas veces entró
    avg_minutes=('minutes', 'mean')     # Tiempo promedio por visita
).reset_index()

# Calculamos repeticiones reales (visitas - 1)
user_interactions['repeticiones'] = user_interactions['visits'] - 1

# 2. AGREGACIÓN NIVEL ARTEFACTO (El Indicador)
difficulty_table = user_interactions.groupby(['label', 'chapter']).agg(
    usuarios_unicos=('_idUser', 'nunique'),     # Cuánta gente pasó por aquí
    total_repeticiones=('repeticiones', 'sum'), # Total bruto de reintentos
    tiempo_promedio=('avg_minutes', 'mean')     # Tiempo promedio global
).reset_index()

# 3. CÁLCULO DE LA TASA DE DIFICULTAD (KPI)
difficulty_table['tasa_reincidencia'] = difficulty_table['total_repeticiones'] / difficulty_table['usuarios_unicos']

# 4. ORDENAR Y LIMPIAR
top_difficulty = difficulty_table.sort_values(by='tasa_reincidencia', ascending=False).head(15)

# Redondeo
top_difficulty = top_difficulty.round(2)

# 5. VISUALIZACIÓN CON ESTILO (Heatmap)
styled_table = top_difficulty.style.background_gradient(
    subset=['tasa_reincidencia', 'tiempo_promedio'],
    cmap='Reds'
).format({
    'tasa_reincidencia': '{:.2f}',
    'tiempo_promedio': '{:.2f} min',
    'total_repeticiones': '{:.0f}',
    'usuarios_unicos': '{:.0f}'
})

# Mostrar la tabla
print("Top 15 Artefactos con mayor Tasa de Reincidencia (Dificultad):")
styled_table
Top 15 Artefactos con mayor Tasa de Reincidencia (Dificultad):
Out[23]:
  label chapter usuarios_unicos total_repeticiones tiempo_promedio tasa_reincidencia
65 C0-P6.0-4 0 846 12593 1.38 min 14.89
371 C1-P8.0-6 1 824 11733 0.99 min 14.24
97 C1-P13.0-2 1 824 11381 0.93 min 13.81
168 C1-P18.0-7 1 786 8816 0.73 min 11.22
67 C0-P6.0-6 0 827 9228 1.47 min 11.16
63 C0-P6.0-2 0 867 9596 1.59 min 11.07
341 C1-P30.1-3 1 713 7487 0.85 min 10.50
202 C1-P2.0-5 1 859 8436 0.68 min 9.82
159 C1-P17.0-6 1 801 7835 0.63 min 9.78
199 C1-P2.0-2 1 859 8173 0.71 min 9.51
64 C0-P6.0-3 0 832 7738 1.70 min 9.30
93 C1-P12.0-4 1 821 7445 0.65 min 9.07
280 C1-P25.0-8 1 739 6636 1.09 min 8.98
59 C0-P5.0-5 0 883 7536 1.33 min 8.53
316 C1-P29.0-8 1 703 5887 0.90 min 8.37
In [32]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Filtramos por el Capítulo 0
df_cap0 = droped_df[droped_df['chapter'] == 0].copy()

# 2. Creamos una etiqueta combinada: "Pág [X] - Art [Y]"
# Ordenamos primero por página y luego por artefacto para que la gráfica sea lógica
df_cap0 = df_cap0.sort_values(by=['page', 'artifact'])
df_cap0['page_artifact'] = "P" + df_cap0['page'].astype(str) + " - " + df_cap0['artifact'].astype(str)

# 3. Configuramos el gráfico
plt.figure(figsize=(16, 8))

# Usamos stripplot para ver a los estudiantes (puntos)
# 'hue' opcionalmente puede ser la página para darles colores distintos por sección
plot = sns.stripplot(
    data=df_cap0, 
    x='page_artifact', 
    y='seconds', 
    hue='page',       # Diferencia las páginas por colores
    jitter=0.25,      # Dispersión horizontal para ver mejor a los estudiantes
    alpha=0.6, 
    palette='Set2', 
    size=7
)

# 4. Estética y legibilidad
plt.title('Tiempo por Estudiante: Artefactos del Capítulo 0 (Organizados por Página)', fontsize=16)
plt.xlabel('Ubicación (Página - Artefacto)', fontsize=12)
plt.ylabel('Tiempo (Segundos)', fontsize=12)
plt.xticks(rotation=45, ha='right') # Rotación para que no se amontonen las etiquetas
plt.legend(title='Nro. Página', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid(axis='y', linestyle='--', alpha=0.3)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [34]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Filtrar por el Capítulo 1
df_cap1 = droped_df[droped_df['chapter'] == 1].copy()

# 2. Ordenar por página y artefacto para mantener la secuencia lógica
df_cap1 = df_cap1.sort_values(by=['page', 'artifact'])

# 3. Crear la etiqueta combinada para el eje X
df_cap1['page_artifact'] = "P" + df_cap1['page'].astype(str) + " - " + df_cap1['artifact'].astype(str)

# 4. Configurar el estilo y tamaño de la gráfica
plt.figure(figsize=(18, 9))

# Graficar puntos individuales (estudiantes)
# He cambiado la paleta a 'Set1' para que visualmente sepas que es otro capítulo
sns.stripplot(
    data=df_cap1, 
    x='page_artifact', 
    y='seconds', 
    hue='page',       # Colores distintos por cada página
    jitter=0.3,       # Esparcimiento horizontal de los puntos
    alpha=0.6,        # Transparencia para ver dónde hay más densidad
    palette='Set1', 
    size=7
)

# 5. Personalización y etiquetas
plt.title('Capítulo 1: Tiempo por Estudiante en cada Artefacto', fontsize=18, fontweight='bold')
plt.xlabel('Página - Artefacto', fontsize=13)
plt.ylabel('Tiempo (Segundos)', fontsize=13)
plt.xticks(rotation=45, ha='right') # Rotación para evitar que los nombres se solapen

# Mover la leyenda fuera de la gráfica para que no estorbe
plt.legend(title='Página', bbox_to_anchor=(1.02, 1), loc='upper left', borderaxespad=0)

plt.grid(axis='y', linestyle=':', alpha=0.5)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [37]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Preparación de datos (Cap 0)
df_fails_0 = droped_df[(droped_df['chapter'] == 0) & (droped_df['validationArtifact'] == False)].copy()
df_counts_0 = df_fails_0.groupby(['_idUser', 'page', 'artifact']).size().reset_index(name='failed_attempts')
df_counts_0 = df_counts_0.sort_values(by=['page', 'artifact'])
df_counts_0['label'] = "P" + df_counts_0['page'].astype(str) + " - " + df_counts_0['artifact'].astype(str)

# 2. Configuración de accesibilidad
plt.figure(figsize=(18, 10))
sns.set_context("talk") # Aumenta el tamaño base de todos los elementos de la fuente

# 3. Gráfica con puntos grandes y definidos
sns.stripplot(
    data=df_counts_0, 
    x='label', 
    y='failed_attempts', 
    hue='page', 
    jitter=0.25, 
    alpha=0.9,           # Menos transparencia para que el color sea sólido
    palette='bright',    # Colores primarios y brillantes
    size=12,             # Puntos mucho más grandes
    edgecolor='black',   # Borde negro para contraste
    linewidth=1          # Grosor del borde
)

# 4. Etiquetas extra grandes
plt.title('CAPÍTULO 0: Intentos Fallidos por Estudiante', fontsize=22, fontweight='bold', pad=20)
plt.xlabel('Página y Artefacto', fontsize=16, fontweight='bold')
plt.ylabel('Cantidad de Intentos Fallidos', fontsize=16, fontweight='bold')
plt.xticks(rotation=45, ha='right', fontsize=14)
plt.yticks(fontsize=14)

plt.legend(title='Página', title_fontsize='15', fontsize='13', bbox_to_anchor=(1, 1))
plt.grid(axis='y', color='gray', linestyle='-', alpha=0.3) # Rejilla más visible

plt.tight_layout()
plt.show()
No description has been provided for this image
In [39]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Preparación de datos (Capítulo 1)
df_fails_1 = droped_df[(droped_df['chapter'] == 1) & (droped_df['validationArtifact'] == False)].copy()
df_counts_1 = df_fails_1.groupby(['_idUser', 'page', 'artifact']).size().reset_index(name='failed_attempts')
df_counts_1 = df_counts_1.sort_values(by=['page', 'artifact'])
df_counts_1['label'] = "P" + df_counts_1['page'].astype(str) + " - " + df_counts_1['artifact'].astype(str)

# 2. Configuración de accesibilidad (Tamaño de fuente "talk" para mejor legibilidad)
plt.figure(figsize=(18, 10))
sns.set_context("talk") 

# 3. Gráfica sin borde negro en los puntos
sns.stripplot(
    data=df_counts_1, 
    x='label', 
    y='failed_attempts', 
    hue='page', 
    jitter=0.25, 
    alpha=0.9,           
    palette='dark',      # Colores fuertes y saturados
    size=12,             # Mantenemos el tamaño grande
    linewidth=0          # <--- Aquí quitamos el borde (0 de grosor)
)

# 4. Textos grandes y claros
plt.title('CAPÍTULO 1: Intentos Fallidos por Estudiante', fontsize=22, fontweight='bold', pad=20)
plt.xlabel('Página y Artefacto', fontsize=16, fontweight='bold')
plt.ylabel('Cantidad de Intentos Fallidos', fontsize=16, fontweight='bold')
plt.xticks(rotation=45, ha='right', fontsize=14)
plt.yticks(fontsize=14)

# Ajustar leyenda y rejilla para que no estorben
plt.legend(title='Nro. Página', title_fontsize='15', fontsize='13', bbox_to_anchor=(1, 1))
plt.grid(axis='y', color='gray', linestyle='-', alpha=0.2)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [40]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Preparación de datos (Cap 0 - Éxitos)
df_success_0 = droped_df[(droped_df['chapter'] == 0) & (droped_df['validationArtifact'] == True)].copy()
df_counts_s0 = df_success_0.groupby(['_idUser', 'page', 'artifact']).size().reset_index(name='success_count')
df_counts_s0 = df_counts_s0.sort_values(by=['page', 'artifact'])
df_counts_s0['label'] = "P" + df_counts_s0['page'].astype(str) + " - " + df_counts_s0['artifact'].astype(str)

# 2. Configuración de accesibilidad
plt.figure(figsize=(18, 10))
sns.set_context("talk") 

# 3. Gráfica con borde negro (Capítulo 0)
sns.stripplot(
    data=df_counts_s0, 
    x='label', 
    y='success_count', 
    hue='page', 
    jitter=0.25, 
    alpha=0.9, 
    palette='bright', 
    size=12, 
    edgecolor='black', 
    linewidth=1.2
)

# 4. Personalización para vista cansada
plt.title('CAPÍTULO 0: Intentos EXITOSOS por Estudiante', fontsize=24, fontweight='bold', color='#1a1a1a')
plt.xlabel('Ubicación (Página - Artefacto)', fontsize=18, fontweight='bold')
plt.ylabel('Cantidad de Veces Completado', fontsize=18, fontweight='bold')
plt.xticks(rotation=45, ha='right', fontsize=14)
plt.yticks(range(int(df_counts_s0['success_count'].max()) + 2), fontsize=14) # Eje Y con números enteros claros

plt.legend(title='Página', title_fontsize='16', fontsize='14', bbox_to_anchor=(1, 1))
plt.grid(axis='y', color='gray', linestyle='-', alpha=0.3)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [41]:
import matplotlib.pyplot as plt
import seaborn as sns

# 1. Preparación de datos (Cap 1 - Éxitos)
df_success_1 = droped_df[(droped_df['chapter'] == 1) & (droped_df['validationArtifact'] == True)].copy()
df_counts_s1 = df_success_1.groupby(['_idUser', 'page', 'artifact']).size().reset_index(name='success_count')
df_counts_s1 = df_counts_s1.sort_values(by=['page', 'artifact'])
df_counts_s1['label'] = "P" + df_counts_s1['page'].astype(str) + " - " + df_counts_s1['artifact'].astype(str)

# 2. Configuración visual
plt.figure(figsize=(18, 10))
sns.set_context("talk")

# 3. Gráfica SIN borde negro (Capítulo 1)
sns.stripplot(
    data=df_counts_s1, 
    x='label', 
    y='success_count', 
    hue='page', 
    jitter=0.3, 
    alpha=1.0,           # Color 100% sólido para compensar la falta de borde
    palette='dark',      # Colores muy fuertes
    size=13,             # Ligeramente más grande para compensar la falta de borde
    linewidth=0          # Sin borde
)

# 4. Textos de alto contraste
plt.title('CAPÍTULO 1: Intentos EXITOSOS por Estudiante', fontsize=24, fontweight='bold', color='#1a1a1a')
plt.xlabel('Ubicación (Página - Artefacto)', fontsize=18, fontweight='bold')
plt.ylabel('Cantidad de Veces Completado', fontsize=18, fontweight='bold')
plt.xticks(rotation=45, ha='right', fontsize=14)
plt.yticks(range(int(df_counts_s1['success_count'].max()) + 2), fontsize=14)

plt.legend(title='Página', title_fontsize='16', fontsize='14', bbox_to_anchor=(1, 1))
plt.grid(axis='y', color='black', linestyle=':', alpha=0.2)

plt.tight_layout()
plt.show()
No description has been provided for this image
In [ ]: